package com.github.syncrepo.service;

import com.github.syncrepo.model.SyncConfig;
import com.github.syncrepo.util.FileUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PublicKey;
import java.util.Collections;
import java.util.List;

/**
 * Core service for Git repository synchronization
 */
public class GitSyncService {
    private static final Logger logger = LoggerFactory.getLogger(GitSyncService.class);
    private static final String SOURCE_REMOTE_NAME = "source";
    private static final String ORIGIN_REMOTE_NAME = "origin";

    /**
     * Synchronize repository from source to target
     */
    public void syncRepository(SyncConfig config) throws Exception {
        config.validate();
        
        logger.info("Starting repository synchronization...");
        logger.info("Source: {} ({}: {})", config.getSourceRepoUrl(), 
                   config.getSourceRefType(), config.getSourceBranch());
        logger.info("Target: {} ({}: {})", config.getTargetRepoUrl(), 
                   config.getTargetRefType(), config.getTargetBranch());
        
        // Route to appropriate sync method based on ref types
        if (config.getSourceRefType() == SyncConfig.RefType.TAG && 
            config.getTargetRefType() == SyncConfig.RefType.TAG) {
            syncTagToTag(config);
        } else if (config.getSourceRefType() == SyncConfig.RefType.TAG && 
                   config.getTargetRefType() == SyncConfig.RefType.BRANCH) {
            syncTagToBranch(config);
        } else if (config.getSourceRefType() == SyncConfig.RefType.COMMIT && 
                   config.getTargetRefType() == SyncConfig.RefType.BRANCH) {
            syncCommitToBranch(config);
        } else if (config.getSourceRefType() == SyncConfig.RefType.BRANCH && 
                   config.getTargetRefType() == SyncConfig.RefType.BRANCH) {
            syncBranchToBranch(config);
        } else {
            throw new IllegalArgumentException(
                "Unsupported synchronization: " + config.getSourceRefType() + " to " + 
                config.getTargetRefType() + ". Supported: BRANCH→BRANCH, TAG→TAG, TAG→BRANCH, COMMIT→BRANCH.");
        }
    }
    
    /**
     * Synchronize tag from source to target
     */
    private void syncTagToTag(SyncConfig config) throws Exception {
        logger.info("Tag to Tag synchronization mode");
        
        File workDir = prepareWorkDirectory(config);
        Git git = null;

        try {
            // Step 1: Clone target repository (shallow clone is enough)
            git = cloneTargetRepository(config, workDir);
            
            // Configure Git LFS environment for SSH (after clone)
            configureGitLfsEnvironment(config, git);

            // Step 2: Add source repository as remote
            addSourceRemote(git, config);

            // Step 3: Fetch source tag
            fetchSourceTag(git, config);

            // Step 4: Check if target tag already exists
            boolean targetTagExists = checkTagExists(git, config.getTargetBranch());
            
            if (targetTagExists) {
                if (config.isForceSync()) {
                    logger.warn("Target tag '{}' already exists, force sync is enabled", 
                               config.getTargetBranch());
                    // Delete local tag
                    git.tagDelete().setTags(config.getTargetBranch()).call();
                } else {
                    throw new Exception(
                        String.format("Tag '%s' already exists in target repository. " +
                                     "Use --force option to overwrite it.", 
                                     config.getTargetBranch()));
                }
            }

            // Step 5: Create target tag (if names differ, otherwise it's already fetched)
            if (!config.getSourceBranch().equals(config.getTargetBranch())) {
                createTargetTag(git, config);
            }

            // Step 6: Push tag to target repository
            pushTagToTarget(git, config);

            logger.info("Tag synchronization completed successfully!");
            logger.info("Target tag '{}' is now synchronized with source tag '{}'", 
                       config.getTargetBranch(), config.getSourceBranch());

        } finally {
            // Cleanup
            if (git != null) {
                git.close();
            }

            if (!config.isKeepLocalCopy()) {
                cleanupWorkDirectory(workDir);
            } else {
                logger.info("Local copy kept at: {}", workDir.getAbsolutePath());
            }
        }
    }
    
    /**
     * Synchronize tag from source to branch in target
     */
    private void syncTagToBranch(SyncConfig config) throws Exception {
        logger.info("Tag to Branch synchronization mode");
        
        File workDir = prepareWorkDirectory(config);
        Git git = null;

        try {
            // Step 1: Clone target repository
            git = cloneTargetRepository(config, workDir);
            
            // Configure Git LFS environment for SSH (after clone)
            configureGitLfsEnvironment(config, git);

            // Step 2: Add source repository as remote
            addSourceRemote(git, config);

            // Step 3: Fetch source tag
            fetchSourceTag(git, config);

            // Step 4: Check if target branch already exists
            boolean targetBranchExists = checkBranchExists(git, config.getTargetBranch());
            
            if (targetBranchExists) {
                logger.warn("Target branch '{}' already exists in target repository", config.getTargetBranch());
                logger.info("Switching to existing branch and updating it to tag '{}'...", config.getSourceBranch());
                
                // Checkout existing branch
                git.checkout()
                        .setName(config.getTargetBranch())
                        .call();
                
                // Reset to source tag (hard reset)
                String sourceTagRef = "refs/tags/" + config.getSourceBranch();
                git.reset()
                        .setMode(org.eclipse.jgit.api.ResetCommand.ResetType.HARD)
                        .setRef(sourceTagRef)
                        .call();
                
                logger.info("Branch updated to match source tag '{}'", config.getSourceBranch());
            } else {
                // Create new branch based on source tag
                createBranchFromTag(git, config);
            }

            // Step 5: Push to target repository
            pushToTarget(git, config, targetBranchExists);

            logger.info("Repository synchronization completed successfully!");
            logger.info("Target branch '{}' is now synchronized with source tag '{}'", 
                       config.getTargetBranch(), config.getSourceBranch());

        } finally {
            // Cleanup
            if (git != null) {
                git.close();
            }

            if (!config.isKeepLocalCopy()) {
                cleanupWorkDirectory(workDir);
            } else {
                logger.info("Local copy kept at: {}", workDir.getAbsolutePath());
            }
        }
    }
    
    /**
     * Synchronize commit from source to branch in target
     */
    private void syncCommitToBranch(SyncConfig config) throws Exception {
        logger.info("Commit to Branch synchronization mode");
        
        File workDir = prepareWorkDirectory(config);
        Git git = null;

        try {
            // Step 1: Clone target repository
            git = cloneTargetRepository(config, workDir);
            
            // Configure Git LFS environment for SSH (after clone)
            configureGitLfsEnvironment(config, git);

            // Step 2: Add source repository as remote
            addSourceRemote(git, config);

            // Step 3: Fetch source commit
            fetchSourceCommit(git, config);

            // Step 4: Check if target branch already exists
            boolean targetBranchExists = checkBranchExists(git, config.getTargetBranch());
            
            if (targetBranchExists) {
                logger.warn("Target branch '{}' already exists in target repository", config.getTargetBranch());
                logger.info("Switching to existing branch and updating it to commit '{}'...", config.getSourceBranch());
                
                // Checkout existing branch
                git.checkout()
                        .setName(config.getTargetBranch())
                        .call();
                
                // Reset to source commit (hard reset)
                git.reset()
                        .setMode(org.eclipse.jgit.api.ResetCommand.ResetType.HARD)
                        .setRef(config.getSourceBranch())
                        .call();
                
                logger.info("Branch updated to match source commit '{}'", config.getSourceBranch());
            } else {
                // Create new branch based on source commit
                createBranchFromCommit(git, config);
            }

            // Step 5: Push to target repository
            pushToTarget(git, config, targetBranchExists);

            logger.info("Repository synchronization completed successfully!");
            logger.info("Target branch '{}' is now synchronized with source commit '{}'", 
                       config.getTargetBranch(), config.getSourceBranch());

        } finally {
            // Cleanup
            if (git != null) {
                git.close();
            }

            if (!config.isKeepLocalCopy()) {
                cleanupWorkDirectory(workDir);
            } else {
                logger.info("Local copy kept at: {}", workDir.getAbsolutePath());
            }
        }
    }
    
    /**
     * Synchronize branch from source to target (original implementation)
     */
    private void syncBranchToBranch(SyncConfig config) throws Exception {

        logger.info("Branch to Branch synchronization mode");
        
        File workDir = prepareWorkDirectory(config);
        Git git = null;

        try {
            // Step 1: Clone target repository
            git = cloneTargetRepository(config, workDir);
            
            // Configure Git LFS environment for SSH (after clone)
            configureGitLfsEnvironment(config, git);

            // Step 2: Add source repository as remote
            addSourceRemote(git, config);

            // Step 3: Fetch source branch
            fetchSourceBranch(git, config);

            // Step 4: Check if target branch already exists
            boolean targetBranchExists = checkBranchExists(git, config.getTargetBranch());
            
            if (targetBranchExists) {
                logger.warn("Target branch '{}' already exists in target repository", config.getTargetBranch());
                logger.info("Switching to existing branch and updating it...");
                
                // Checkout existing branch
                git.checkout()
                        .setName(config.getTargetBranch())
                        .call();
                
                // Reset to source branch (hard reset)
                String sourceBranchRef = SOURCE_REMOTE_NAME + "/" + config.getSourceBranch();
                git.reset()
                        .setMode(org.eclipse.jgit.api.ResetCommand.ResetType.HARD)
                        .setRef(sourceBranchRef)
                        .call();
                
                logger.info("Branch updated to match source");
            } else {
                // Create new branch based on source branch
                createTargetBranch(git, config);
            }

            // Step 5: Push to target repository
            pushToTarget(git, config, targetBranchExists);

            logger.info("Repository synchronization completed successfully!");
            logger.info("Target branch '{}' is now synchronized with source branch '{}'", 
                       config.getTargetBranch(), config.getSourceBranch());

        } finally {
            // Cleanup
            if (git != null) {
                git.close();
            }

            if (!config.isKeepLocalCopy()) {
                cleanupWorkDirectory(workDir);
            } else {
                logger.info("Local copy kept at: {}", workDir.getAbsolutePath());
            }
        }
    }

    /**
     * Prepare working directory
     */
    private File prepareWorkDirectory(SyncConfig config) throws IOException {
        if (config.getWorkDir() != null && !config.getWorkDir().isEmpty()) {
            return FileUtils.ensureDirectory(config.getWorkDir());
        }
        return FileUtils.createTempDirectory("sync-repo-");
    }

    /**
     * Check if URL is SSH format (e.g., git@github.com:user/repo.git)
     */
    private boolean isSshUrl(String url) {
        return url != null && (url.startsWith("git@") || url.startsWith("ssh://"));
    }

    /**
     * Create credentials provider based on repository URL
     * Returns null for SSH URLs (will use SSH keys instead)
     * GitCode/GitLab requires "oauth2" as username, GitHub uses token as username
     */
    private UsernamePasswordCredentialsProvider createCredentialsProvider(String repoUrl, String token) {
        // SSH URLs don't need credentials provider - will use SSH keys
        if (isSshUrl(repoUrl)) {
            logger.debug("SSH URL detected, will use SSH key authentication");
            return null;
        }
        
        // For HTTP/HTTPS URLs, use token authentication
        // GitCode and GitLab use "oauth2" or "git" as username with token as password
        if (repoUrl.contains("gitcode.com") || repoUrl.contains("gitlab.com")) {
            logger.debug("Using 'oauth2' authentication for GitCode/GitLab");
            return new UsernamePasswordCredentialsProvider("oauth2", token);
        }
        // Gitee uses token as username
        else if (repoUrl.contains("gitee.com")) {
            logger.debug("Using token authentication for Gitee");
            return new UsernamePasswordCredentialsProvider(token, "");
        }
        // GitHub and others use token as username with empty password
        else {
            logger.debug("Using token authentication for GitHub/generic");
            return new UsernamePasswordCredentialsProvider(token, "");
        }
    }

    /**
     * Create SSH transport callback for SSH connections
     */
    private TransportConfigCallback createSshTransportCallback(SyncConfig config) {
        return new TransportConfigCallback() {
            @Override
            public void configure(Transport transport) {
                if (transport instanceof SshTransport) {
                    SshTransport sshTransport = (SshTransport) transport;
                    
                    // Create custom session factory with server key acceptance
                    SshdSessionFactoryBuilder builder = new SshdSessionFactoryBuilder()
                            .setPreferredAuthentications("publickey")
                            // Accept all server keys (similar to StrictHostKeyChecking=no)
                            .setServerKeyDatabase((homeDir, sshDir) -> new ServerKeyDatabase() {
                                @Override
                                public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) {
                                    return Collections.emptyList();
                                }

                                @Override
                                public boolean accept(String connectAddress, InetSocketAddress remoteAddress, 
                                                    PublicKey serverKey, Configuration config, CredentialsProvider provider) {
                                    // Accept all server keys
                                    return true;
                                }
                            });
                    
                    // Use custom SSH key if specified
                    if (config.getSshKeyPath() != null && !config.getSshKeyPath().isEmpty()) {
                        logger.info("Using custom SSH key: {}", config.getSshKeyPath());
                        File sshKey = new File(config.getSshKeyPath());
                        
                        if (!sshKey.exists()) {
                            logger.warn("SSH key file not found: {}, will try default keys", config.getSshKeyPath());
                        } else {
                            builder.setHomeDirectory(FS.DETECTED.userHome())
                                   .setSshDirectory(sshKey.getParentFile());
                        }
                    } else {
                        // Use default SSH keys from ~/.ssh/
                        logger.debug("Using default SSH keys from ~/.ssh/");
                        File sshDir = new File(FS.DETECTED.userHome(), ".ssh");
                        builder.setHomeDirectory(FS.DETECTED.userHome())
                               .setSshDirectory(sshDir);
                    }
                    
                    SshdSessionFactory factory = builder.build(null);
                    sshTransport.setSshSessionFactory(factory);
                }
            }
        };
    }

    /**
     * Configure environment for Git LFS
     * Ensures Git LFS can use custom SSH key for subsequent operations
     * Note: Initial clone already configures this, but this method ensures
     * the configuration persists for fetch/push operations
     */
    private File configureGitLfsEnvironment(SyncConfig config, Git git) throws IOException {
        boolean isSsh = isSshUrl(config.getSourceRepoUrl()) || isSshUrl(config.getTargetRepoUrl());
        
        if (!isSsh) {
            return null;
        }
        
        String sshKeyPath = config.getSshKeyPath();
        if (sshKeyPath == null || sshKeyPath.isEmpty()) {
            // Use default SSH keys
            logger.debug("Using default SSH configuration for Git LFS");
            return null;
        }
        
        if (git != null) {
            // Verify/update core.sshCommand in git config
            // This is usually already set during clone, but we verify it here
            try {
                org.eclipse.jgit.lib.StoredConfig config2 = git.getRepository().getConfig();
                String existingCommand = config2.getString("core", null, "sshCommand");
                
                if (existingCommand == null || !existingCommand.contains(sshKeyPath)) {
                    config2.setString("core", null, "sshCommand", 
                        String.format("ssh -i %s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null", sshKeyPath));
                    config2.save();
                    logger.info("Configured Git core.sshCommand with SSH key: {}", sshKeyPath);
                } else {
                    logger.debug("Git core.sshCommand already configured");
                }
            } catch (Exception e) {
                logger.warn("Failed to configure Git sshCommand: {}", e.getMessage());
            }
        }
        
        logger.debug("Git LFS will use SSH key: {}", sshKeyPath);
        
        return null;  // No longer need SSH wrapper script
    }

    /**
     * Clone target repository
     * For repositories with LFS files and custom SSH keys, clone without checkout first,
     * then configure LFS environment and checkout manually
     */
    private Git cloneTargetRepository(SyncConfig config, File workDir) throws GitAPIException, IOException {
        logger.info("Cloning target repository...");
        
        boolean isSsh = isSshUrl(config.getTargetRepoUrl());
        UsernamePasswordCredentialsProvider credentialsProvider = null;
        
        if (!isSsh) {
            credentialsProvider = createCredentialsProvider(config.getTargetRepoUrl(), config.getAccessToken());
        }
        
        // Check if we need to handle LFS with custom SSH key
        String sshKeyPath = config.getSshKeyPath();
        boolean needsLfsConfig = isSsh && sshKeyPath != null && !sshKeyPath.isEmpty();

        var cloneCommand = Git.cloneRepository()
                .setURI(config.getTargetRepoUrl())
                .setDirectory(workDir)
                .setCloneAllBranches(false);
        
        // If using custom SSH key for LFS, clone without checkout first
        if (needsLfsConfig) {
            logger.info("Cloning without checkout (will configure LFS environment first)...");
            cloneCommand.setNoCheckout(true);
        }
        
        if (credentialsProvider != null) {
            cloneCommand.setCredentialsProvider(credentialsProvider);
        }
        
        if (isSsh) {
            cloneCommand.setTransportConfigCallback(createSshTransportCallback(config));
        }
        
        Git git = cloneCommand.call();
        
        // If we cloned without checkout, configure LFS and then checkout
        if (needsLfsConfig) {
            logger.info("Configuring Git LFS environment with SSH key...");
            
            try {
                org.eclipse.jgit.lib.StoredConfig gitConfig = git.getRepository().getConfig();
                gitConfig.setString("core", null, "sshCommand", 
                    String.format("ssh -i %s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null", sshKeyPath));
                gitConfig.save();
                logger.info("Configured Git core.sshCommand for LFS");
            } catch (Exception e) {
                logger.warn("Failed to configure Git sshCommand: {}", e.getMessage());
            }
            
            // Check if repository has any commits before attempting checkout
            try {
                ObjectId head = git.getRepository().resolve("HEAD");
                if (head != null) {
                    logger.info("Checking out HEAD...");
                    git.checkout().setName("HEAD").call();
                    logger.info("Checkout completed");
                } else {
                    logger.info("Target repository is empty (no HEAD), skipping checkout");
                }
            } catch (org.eclipse.jgit.api.errors.RefNotFoundException e) {
                logger.info("Target repository is empty (HEAD not found), skipping checkout");
            } catch (IOException e) {
                logger.warn("Failed to resolve HEAD: {}", e.getMessage());
            }
        }

        logger.info("Target repository cloned successfully");
        return git;
    }

    /**
     * Add source repository as remote
     */
    private void addSourceRemote(Git git, SyncConfig config) throws URISyntaxException, GitAPIException {
        logger.info("Adding source repository as remote '{}'...", SOURCE_REMOTE_NAME);
        
        git.remoteAdd()
                .setName(SOURCE_REMOTE_NAME)
                .setUri(new URIish(config.getSourceRepoUrl()))
                .call();

        logger.info("Source remote added successfully");
    }

    /**
     * Fetch source branch from source repository
     */
    private void fetchSourceBranch(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Fetching source branch '{}' from source repository...", config.getSourceBranch());
        
        RefSpec refSpec = new RefSpec(
                "refs/heads/" + config.getSourceBranch() + ":refs/remotes/" + 
                SOURCE_REMOTE_NAME + "/" + config.getSourceBranch()
        );

        boolean isSsh = isSshUrl(config.getSourceRepoUrl());
        var fetchCommand = git.fetch()
                .setRemote(SOURCE_REMOTE_NAME)
                .setRefSpecs(refSpec);
        
        if (isSsh) {
            fetchCommand.setTransportConfigCallback(createSshTransportCallback(config));
        } else {
            UsernamePasswordCredentialsProvider credentialsProvider = 
                    createCredentialsProvider(config.getSourceRepoUrl(), config.getAccessToken());
            if (credentialsProvider != null) {
                fetchCommand.setCredentialsProvider(credentialsProvider);
            }
        }
        
        fetchCommand.call();

        logger.info("Source branch fetched successfully");
    }

    /**
     * Check if a branch exists in the repository
     */
    private boolean checkBranchExists(Git git, String branchName) throws GitAPIException {
        List<Ref> branches = git.branchList().call();
        String branchRef = "refs/heads/" + branchName;
        
        for (Ref ref : branches) {
            if (ref.getName().equals(branchRef)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Create target branch based on source branch
     */
    private void createTargetBranch(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Creating target branch '{}'...", config.getTargetBranch());
        
        String startPoint = SOURCE_REMOTE_NAME + "/" + config.getSourceBranch();
        
        git.checkout()
                .setCreateBranch(true)
                .setName(config.getTargetBranch())
                .setStartPoint(startPoint)
                .call();

        logger.info("Target branch created successfully");
    }
    
    /**
     * Create target branch based on source tag
     */
    private void createBranchFromTag(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Creating target branch '{}' from source tag '{}'...", 
                   config.getTargetBranch(), config.getSourceBranch());
        
        String startPoint = "refs/tags/" + config.getSourceBranch();
        
        git.checkout()
                .setCreateBranch(true)
                .setName(config.getTargetBranch())
                .setStartPoint(startPoint)
                .call();

        logger.info("Target branch created successfully from tag");
    }
    
    /**
     * Create target branch based on source commit
     */
    private void createBranchFromCommit(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Creating target branch '{}' from source commit '{}'...", 
                   config.getTargetBranch(), config.getSourceBranch());
        
        // The commit ID is already fetched, use it directly
        git.checkout()
                .setCreateBranch(true)
                .setName(config.getTargetBranch())
                .setStartPoint(config.getSourceBranch())
                .call();

        logger.info("Target branch created successfully from commit");
    }

    /**
     * Push target branch to target repository
     */
    private void pushToTarget(Git git, SyncConfig config, boolean forcePush) throws GitAPIException {
        logger.info("Pushing to target repository...");
        
        boolean isSsh = isSshUrl(config.getTargetRepoUrl());
        
        try {
            var pushCommand = git.push()
                    .setRemote(ORIGIN_REMOTE_NAME)
                    .setRefSpecs(new RefSpec(config.getTargetBranch() + ":" + config.getTargetBranch()));
            
            if (isSsh) {
                pushCommand.setTransportConfigCallback(createSshTransportCallback(config));
            } else {
                UsernamePasswordCredentialsProvider credentialsProvider = 
                        createCredentialsProvider(config.getTargetRepoUrl(), config.getAccessToken());
                if (credentialsProvider != null) {
                    pushCommand.setCredentialsProvider(credentialsProvider);
                }
            }
            
            // Force push if updating existing branch
            if (forcePush) {
                pushCommand.setForce(true);
                logger.info("Force pushing to update existing branch...");
            }
            
            pushCommand.call();

            logger.info("Push completed successfully");
            
        } catch (org.eclipse.jgit.api.errors.TransportException e) {
            logger.error("Push failed: {}", e.getMessage());
            
            if (e.getMessage().contains("not authorized") || e.getMessage().contains("authentication failed")) {
                String authMessage;
                if (isSsh) {
                    authMessage = "SSH Authentication failed! Please check:\n" +
                        "  1. Your SSH keys are properly configured (~/.ssh/id_rsa or ~/.ssh/id_ed25519)\n" +
                        "  2. SSH public key is added to your Git platform account\n" +
                        "  3. You have write access to the target repository: " + config.getTargetRepoUrl() + "\n" +
                        "  4. SSH agent is running and key is loaded (ssh-add -l)\n" +
                        "  5. Test SSH connection: ssh -T git@<host>\n\n" +
                        "For large repositories, SSH is recommended over HTTPS.";
                } else {
                    authMessage = "Authentication failed! Please check:\n" +
                        "  1. Your access token is correct and not expired\n" +
                        "  2. Token has appropriate permissions (repository read/write access)\n" +
                        "  3. You have write access to the target repository: " + config.getTargetRepoUrl() + "\n" +
                        "  4. If it's an organization repository, token has organization access\n\n" +
                        "For GitCode, ensure:\n" +
                        "  - Token has 'api' or 'write_repository' scope\n" +
                        "  - You are a member/maintainer of the repository\n" +
                        "  - Generate token at: https://gitcode.com/-/user_settings/personal_access_tokens\n\n" +
                        "For GitHub:\n" +
                        "  - Token has 'repo' scope\n" +
                        "  - Generate token at: https://github.com/settings/tokens\n\n" +
                        "For large repositories, consider using SSH URLs instead.";
                }
                
                throw new GitAPIException(authMessage) {
                    @Override
                    public String getMessage() {
                        return super.getMessage();
                    }
                };
            }
            throw e;
        }
    }

    /**
     * Cleanup working directory
     */
    private void cleanupWorkDirectory(File workDir) {
        try {
            logger.info("Cleaning up working directory...");
            FileUtils.deleteDirectory(workDir);
        } catch (IOException e) {
            logger.warn("Failed to cleanup working directory: {}", e.getMessage());
        }
    }
    
    /**
     * Fetch source tag from source repository
     */
    private void fetchSourceTag(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Fetching source tag '{}' from source repository...", config.getSourceBranch());
        
        RefSpec refSpec = new RefSpec(
                "refs/tags/" + config.getSourceBranch() + ":refs/tags/" + config.getSourceBranch()
        );

        boolean isSsh = isSshUrl(config.getSourceRepoUrl());
        var fetchCommand = git.fetch()
                .setRemote(SOURCE_REMOTE_NAME)
                .setRefSpecs(refSpec)
                .setTagOpt(org.eclipse.jgit.transport.TagOpt.FETCH_TAGS);
        
        if (isSsh) {
            fetchCommand.setTransportConfigCallback(createSshTransportCallback(config));
        } else {
            UsernamePasswordCredentialsProvider credentialsProvider = 
                    createCredentialsProvider(config.getSourceRepoUrl(), config.getAccessToken());
            if (credentialsProvider != null) {
                fetchCommand.setCredentialsProvider(credentialsProvider);
            }
        }
        
        fetchCommand.call();

        logger.info("Source tag fetched successfully");
    }
    
    /**
     * Fetch source commit from source repository
     */
    private void fetchSourceCommit(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Fetching source commit '{}' from source repository...", config.getSourceBranch());
        
        // Fetch all refs from source to get the commit
        // We can't directly fetch a specific commit ID, so we fetch all branches
        boolean isSsh = isSshUrl(config.getSourceRepoUrl());
        var fetchCommand = git.fetch()
                .setRemote(SOURCE_REMOTE_NAME);
        
        if (isSsh) {
            fetchCommand.setTransportConfigCallback(createSshTransportCallback(config));
        } else {
            UsernamePasswordCredentialsProvider credentialsProvider = 
                    createCredentialsProvider(config.getSourceRepoUrl(), config.getAccessToken());
            if (credentialsProvider != null) {
                fetchCommand.setCredentialsProvider(credentialsProvider);
            }
        }
        
        fetchCommand.call();

        logger.info("Source commits fetched successfully");
        logger.info("Will use commit: {}", config.getSourceBranch());
    }
    
    /**
     * Check if a tag exists in the repository
     */
    private boolean checkTagExists(Git git, String tagName) throws GitAPIException {
        List<Ref> tags = git.tagList().call();
        String tagRef = "refs/tags/" + tagName;
        
        for (Ref ref : tags) {
            if (ref.getName().equals(tagRef)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Create target tag based on source tag
     */
    private void createTargetTag(Git git, SyncConfig config) throws Exception {
        logger.info("Creating target tag '{}'...", config.getTargetBranch());
        
        // Get the source tag reference
        Ref sourceTagRef = git.getRepository()
                .exactRef("refs/tags/" + config.getSourceBranch());
        
        if (sourceTagRef == null) {
            throw new Exception("Source tag '" + config.getSourceBranch() + "' not found");
        }
        
        // Parse the ObjectId to RevObject using RevWalk
        ObjectId objectId = sourceTagRef.getObjectId();
        RevWalk revWalk = new RevWalk(git.getRepository());
        try {
            RevObject revObject = revWalk.parseAny(objectId);
            
            // Create new tag pointing to the same object
            git.tag()
                    .setName(config.getTargetBranch())
                    .setObjectId(revObject)
                    .setAnnotated(false)  // Create lightweight tag
                    .call();
        } finally {
            revWalk.close();
        }

        logger.info("Target tag created successfully");
    }
    
    /**
     * Push tag to target repository
     */
    private void pushTagToTarget(Git git, SyncConfig config) throws GitAPIException {
        logger.info("Pushing tag to target repository...");
        
        boolean isSsh = isSshUrl(config.getTargetRepoUrl());

        try {
            RefSpec pushRefSpec = new RefSpec(
                "refs/tags/" + config.getTargetBranch() + ":refs/tags/" + config.getTargetBranch()
            );
            
            var pushCommand = git.push()
                    .setRemote(ORIGIN_REMOTE_NAME)
                    .setRefSpecs(pushRefSpec);
            
            if (isSsh) {
                pushCommand.setTransportConfigCallback(createSshTransportCallback(config));
            } else {
                UsernamePasswordCredentialsProvider credentialsProvider = 
                        createCredentialsProvider(config.getTargetRepoUrl(), config.getAccessToken());
                if (credentialsProvider != null) {
                    pushCommand.setCredentialsProvider(credentialsProvider);
                }
            }
            
            // Force push if enabled
            if (config.isForceSync()) {
                pushCommand.setForce(true);
                logger.info("Force pushing tag...");
            }
            
            pushCommand.call();

            logger.info("Tag pushed successfully");
            
        } catch (org.eclipse.jgit.api.errors.TransportException e) {
            logger.error("Push failed: {}", e.getMessage());
            
            if (e.getMessage().contains("not authorized") || e.getMessage().contains("authentication failed")) {
                String authMessage;
                if (isSsh) {
                    authMessage = "SSH Authentication failed! Please check:\n" +
                        "  1. Your SSH keys are properly configured (~/.ssh/id_rsa or ~/.ssh/id_ed25519)\n" +
                        "  2. SSH public key is added to your Git platform account\n" +
                        "  3. You have write access to the target repository: " + config.getTargetRepoUrl() + "\n" +
                        "  4. SSH agent is running and key is loaded (ssh-add -l)\n" +
                        "  5. Test SSH connection: ssh -T git@<host>\n\n" +
                        "For large repositories, SSH is recommended over HTTPS.";
                } else {
                    authMessage = "Authentication failed! Please check:\n" +
                        "  1. Your access token is correct and not expired\n" +
                        "  2. Token has appropriate permissions (repository read/write access)\n" +
                        "  3. You have write access to the target repository: " + config.getTargetRepoUrl() + "\n" +
                        "  4. If it's an organization repository, token has organization access\n\n" +
                        "For GitCode, ensure:\n" +
                        "  - Token has 'api' or 'write_repository' scope\n" +
                        "  - You are a member/maintainer of the repository\n" +
                        "  - Generate token at: https://gitcode.com/-/user_settings/personal_access_tokens\n\n" +
                        "For GitHub:\n" +
                        "  - Token has 'repo' scope\n" +
                        "  - Generate token at: https://github.com/settings/tokens\n\n" +
                        "For large repositories, consider using SSH URLs instead.";
                }
                
                throw new GitAPIException(authMessage) {
                    @Override
                    public String getMessage() {
                        return super.getMessage();
                    }
                };
            }
            throw e;
        }
    }
}
